Pythonプロジェクトに静的解析ツールのRust製Ruffを導入してflake8/black/isortを置き換えてみた
Pythonプロジェクトでflake8/black/isortといった静的解析プログラムと設定ファイルが取っ散らかっていませんか?
Ruffを利用すると、設定ファイルをpyproject.toml
に集約し、次の3コマンドを叩くだけで、リンターを走らせ、コード整形できます。
$ curl -LsSf https://astral.sh/ruff/install.sh | sh
$ ruff check --fix
$ ruff format
Python用静的解析ツール Ruff 導入の背景
最近関わるようになったプロジェクトでは、アプリケーションにPython、IaCにTypeScriptが利用されています。
IaC(AWS CDK)のTypeScriptはリンターやコード整形といった静的解析が自動テストの一環で行われているのに対して、頻繁に更新するアプリケーションのPythonは、各自が手元で各コマンドを個別に実行することを期待する運用になっており、コードレビュー時にレビュアーが静的解析の実行を指摘している状況でした。
Python側にも静的解析の自動テストを導入して、レビュー開始時にTypeScriptと同程度の品質を担保したいよねという話になり、そこで現れたのが Ruff です。
静的解析の人気プログラム(flake8/black/isort)、及び、設定ファイル(pyproject.toml
)と互換性があり、1プログラムに集約でき、Rust製で高速に動作し、GitHub ActionsやVSCodeなどと連携機能が容易であり、FastAPI、Pandas、Apache Superset等の人気OSSでの導入実績も豊富なことから、新興のRuffを導入することにしました。
Pythonの静的解析に求められること
Pythonの静的解析は、リンター(flake8
)、コード整形(black
)、インポート整形(isort
)など機能ごとにツールが乱立していましたが、現在は新興のRuffに集約できます。
Pythonの静的解析に求められることを確認します。
リンター
リンターを利用すると、不要なインポートや定義されていない変数を検知できます。
import os # 不要なインポート
print(n) # 定義されていない変数
リンターとして従来は Flake8 などが有名でした。
$ flake8 sample_linter.py
sample_linter.py:1:1: F401 'os' imported but unused
sample_linter.py:2:7: F821 undefined name 'n'
現在は ruff check
で置き換えられます。
$ ruff check sample_linter.py # 検出
sample_linter.py:1:8: F401 [*] `os` imported but unused
|
1 | import os
| ^^ F401
2 | print(n)
|
= help: Remove unused import: `os`
sample_linter.py:2:7: F821 Undefined name `n`
|
1 | import os
2 | print(n)
| ^ F821
|
Found 2 errors.
[*] 1 fixable with the `--fix` option.
$ ruff check --fix sample_linter.py # `--fix` で修正
sample_linter.py:1:7: F821 Undefined name `n`
|
1 | print(n)
| ^ F821
|
Found 2 errors (1 fixed, 1 remaining).
$ cat sample_linter.py
print(n)
コード整形
複数人で開発するときは、変数の定義、関数の引数、if
文の書き方といったコーディング規約が統一されていると、保守しやすいです。そこで登場するのがコード整形です。
整形前
if n:print("ok")
整形後
if n:
print("ok")
コード整形として従来は Black などが有名でした
$ black sample_format.py
reformatted sample_format.py
All done! ✨ 🍰 ✨
1 file reformatted.
現在は ruff format
で置き換えられます。
$ ruff format --check sample_format.py # コード整形の必要製の検出
Would reformat: sample_format.py
1 file would be reformatted
$ ruff format sample_format.py # コード整形
1 file reformatted
インポート整形
インポート文をアルファベット順にソートしたり、標準・非標準で分けたいことがあります。
整形前
import requests
import sys
import os
整形後
import os
import sys
import requests
インポート文整形として従来は isort などが有名でした
$ isort sample_import.py
Fixing /Users/user/sample_import.py
現在は ruff check --extemd-select I
で置き換えられます。
$ ruff check --extemd-select I --fix sample_import.py
Found 1 error (1 fixed, 0 remaining).
インポート文の整形はRuffリンターの一機能
isort
相当の機能について、Ruff のコード整形(ruff format
)はインポート文をソートしないため、リンター機能(ruff check
)で対応します。
Currently, the Ruff formatter does not sort imports. In order to both sort imports and format, call the Ruff linter and then the formatter:
ruff check --select I --fix
ruff formatA unified command for both linting and formatting is planned.
統合が予定されているとはいえ、多くの人がほしいのは、不要なインポートは削除し、必要なインポートをソートするようなコード整形ではないかと思います。
既存のリンター機能(ruff check
)にデフォルトでは無効な isort
機能を有効化することで(--extend-select
)、インポート文の整理とソートができます。
参考
- It's not possible to remove unused import AND sort them · Issue #10882 · astral-sh/ruff · GitHub
- Unified command for linting and formatting · Issue #8232 · astral-sh/ruff · GitHub
Git フック連携
Gitのコミット時にフック機能を利用して、静的解析を走らせる方法を紹介します。
まず、pre-commit をインストールします。
$ brew install pre-commit
次にレポジトリのトップディレクトリに、次の .pre-commit-config.yaml
ファイルを用意します。
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.6
hooks:
- id: ruff
args: [--extend-select, I, --fix]
- id: ruff-format
最後に、フックスクリプトをデプロイします。
$ pre-commit install
pre-commit installed at .git/hooks/pre-commit
コミット時に自動的にRuffの静的解析が走るようになります。
$ git commit -m test
[WARNING] Unstaged files detected.
[INFO] Stashing unstaged files to /Users/user/.cache/pre-commit/patch1723031830-70446.
ruff.....................................................................Failed
- hook id: ruff
- exit code: 1
- files were modified by this hook
a.py:1:7: F821 Undefined name `n`
|
1 | print(n)
| ^ F821
|
Found 2 errors (1 fixed, 1 remaining).
ruff-format..............................................................Passed
[INFO] Restored changes from /Users/user/.cache/pre-commit/patch1723031830-70446.
参考
VSCode連携
VSCodeの保存時にRuffの静的解析を走らせる方法を紹介します。
Ruffの開発元のAstral Softwareが提供しているVSCode Extentionをインストールします。
ニアリアルタイムにRuffが実行されます
特に、 .vscode/settings.json
に次の設定を追加すると、保存時に Ruff を走らせることができます。
{
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
}
}
}
参考
GitHub Action連携
RuffはGitHub Actionとの連携もスムーズです。
GitHub Actionで細かく制御した場合は、Ruffを直接インストールし、オプションを細かく指定すると良いでしょう。
name: Linter
on: [ push, pull_request ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ruff
# Update output format to enable automatic inline annotations.
- name: Run Ruff
run: ruff check --output-format=github
- name: Run Ruff
run: ruff format --check
凝った設定が不要な場合は、 ruff-action に寄せるといいでしょう。
name: Linter
on: [ push, pull_request ]
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: chartboost/ruff-action@v1
with:
args: check --output-format=github
- uses: chartboost/ruff-action@v1
with:
args: format --check
pyproject.toml設定例
Ruff の大きなメリットの一つは、各機能の設定を pyproject.toml
に集約できることです。
もともとデフォルト値で Black 等を走らせていたことや、パラメーターに凝りだすと運用負荷が増すため、ほぼデフォルト値のままとしています。
[tool.ruff]
target-version = "py310"
[tool.ruff.lint]
ignore = [
"E501", # Line too long
]
select = [
"E", # pycodestyle
"F", # pyflakes
"I", # isort
]
デフォルトとの目立った違いについて触れます。
isort の有効化
isort
のインポート文整形をしたかったため、select
に I(isort)
を追加しています。
行の長さをどうするか?
既存コードベースの都合から、長い行に対するエラー(E501)を除外しています。
以下のようにして、コード整形(line-length
)とE501(max-line-length
)に異なる長さを指定することもできます。
[tool.ruff]
# The formatter wraps lines at a length of 88.
line-length = 88
[tool.ruff.lint.pycodestyle]
# E501 reports lines that exceed the length of 200.
max-line-length = 200
長い1行のままにしたほうが見通しが良い場合は、 # noqa: E501
のコメントを追加して除外することも可能です。
また、$ ruff format
のあと $ ruff check
を走らせると、日本語のようなマルチバイトを含んだ行が E501 で引っかかるケースに遭遇します。文字数ではなくUnicode widthの幅を利用しているのが原因のようです。
参考
特定行を静的解析対象から外す
Ruff は Flake8 と同様に、行の末尾に # noqa
あるいは # noqa: {code}
のコメントを追記すると、警告を抑制できます
x = 1 # noqa
"""Lorem ipsum dolor sit amet.
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
""" # noqa: E501
Ruff 実行時のステータスコード
Ruff の実行結果によるステータスコードの違いを確認します。
check 時
リンターの ruff check
実行時は、異常を検知すると、異常系の非0のステータスコードを返します。
$ ruff check a.py
a.py:1:7: F821 Undefined name `n`
|
1 | print(n)
| ^ F821
|
Found 1 error.
$ echo $?
1
利用されていないインポート文の削除のような修正(--fix
)をした時は、正常ステータスの 0 を返します。
$ ruff check --fix fix.py
Found 1 error (1 fixed, 0 remaining).
$ echo $?
0
このような修正のケースで異常系の1を返したい時は --exit-non-zero-on-fix
オプションを追加します。
$ ruff check --fix --exit-non-zero-on-fix fix.py
Found 1 error (1 fixed, 0 remaining).
$ echo $?
1
format 時
コード整形のruff format
実行時は、正常ステータスの 0 を返します。
$ ruff format b.py
1 file reformatted
$ echo $?
0
コード整形(の候補検出)を異常とみなしたい場合は、--check
オプションを追加して下さい。
$ ruff format --check b.py
Would reformat: b.py
1 file would be reformatted
$ echo $?
1
まとめ
Pythonの静的解析をRuffに集約させ、GitHub Action/VSCode/pre-commit等と連携する方法を紹介しました。
isort
相当のソート文をソートする機能の実現と 整形・E501と関連する line-length
のニュアンスに少しハマりましたが、それ以外については、各種互換性や連携機能のおかげで、非常にスムーズに導入できました。